package com.baidu.fsg.uid.worker;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.io.*;
import java.nio.channels.FileLock;

/**
 * 复用workerIdAssigner装饰器
 *
 * 将获取到的workerId保存到本地机器的文件上，复用之前的workerId
 *
 *
 * 同机多应用 每个应用可以使用不同workerId
 *
 * 注意，每一个UidGenerator对象需要使用不同的wrapper对象， 因为每个wrapper对象只能维护一个lockFile对象，不然会存在(.lock)文件不删除的情况
 * 不同wrapper对象中的innerAssigner可以相同
 *
 *
 * (.lock)文件的删除依赖于spring管理机制， Ctrl+C 停掉应用可以正常删除
 *
 * {@code
 *
 *
 *      @Bean("markWorkerIdAssigner2")
 *     public WorkerIdAssigner createMarkWorkerIdAssigner(
 *             @Qualifier("disposableWorkerIdAssigner") WorkerIdAssigner assigner){
 *         MarkWorkerIdAssignerWrapper wrapper=new MarkWorkerIdAssignerWrapper(assigner);
 *         return wrapper;
 *     }
 *
 *     @Bean("uidGenerator2")
 *     public CachedUidGenerator uidGenerator2(@Qualifier("markWorkerIdAssigner2") WorkerIdAssigner assigner){
 *         CachedUidGenerator uidGenerator=new CachedUidGenerator();
 *         uidGenerator.setWorkerIdAssigner(assigner);
 *         uidGenerator.setEpochStr("2020-01-01"); //起始时间
 *         uidGenerator.setTimeBits(31); // 约可支持69.6年, 一旦投入生产，时间将不可更改，不然会重复， 利用时间的递增性，达到单个机器id的不重复
 *         uidGenerator.setWorkerBits(17); // 机器id  13w次 由于使用了markWorkerId，可以调小一点   以下两个参数可以改
 *         uidGenerator.setSeqBits(15); //  可支持每秒32768 个并发
 *
 *         return uidGenerator;
 *     }
 *
 * }
 *
 * @see com.ttx.uid.config.UidDefaultGeneratorConfig
 *
 * @author TimFruit
 * @date 20-4-9 下午8:01
 */
public class MarkWorkerIdAssignerWrapper implements WorkerIdAssigner,DisposableBean,InitializingBean {

    private WorkerIdAssigner innerAssigner;
    private static final Logger LOGGER = LoggerFactory.getLogger(MarkWorkerIdAssignerWrapper.class);
    private String DATA_ROOT="uid";
    private String WORKER_ID_FOLDER=null;

    // work-id-index.txt
    private int WORKER_ID_DEFAULT_INDEX=0;
    private String WORKER_ID_FILE_NAME_PREFIX="work-id";
    private String WORKER_ID_SEP="-";
    private String WORKER_ID_FILE_NAME_SUFFIX =".txt";
    private String WORKER_ID_FILE_LOCK_SUFFIX =".lock";



    private File workerIdFile=null;
    private String rafMode="rw";
    private File workerIdLockFile=null;

    public MarkWorkerIdAssignerWrapper(WorkerIdAssigner innerAssigner) {
        this.innerAssigner = innerAssigner;
    }




    public void setDataRoot(String dataRoot){
        if(StringUtils.isEmpty(dataRoot)){
            LOGGER.warn("设置的dataRoot为空，使用默认的dataRoot: {}",DATA_ROOT);
            return;
        }
        this.DATA_ROOT=dataRoot;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        File root=new File(DATA_ROOT);
        if(!root.exists()){
            root.mkdirs();
        }

        WORKER_ID_FOLDER=DATA_ROOT.concat(File.separator+"worker-id");//workid文件夹
        File worker=new File(WORKER_ID_FOLDER);
        if(!worker.exists()){
            worker.mkdirs();
        }
    }

    @Override
    public long assignWorkerId() {
        // 由于是程序启动时调用，后续不在调用，所以简单粗暴加锁
        // 之所以使用class对象加锁，因为该类是wrapper类，可能有多个对象
        synchronized (MarkWorkerIdAssignerWrapper.class){
            Long workerId= readWorkerId();
            if(workerId!=null){
                return workerId;
            }
            workerId=this.innerAssigner.assignWorkerId();
            writeWorkerId(workerId);
            return workerId;
        }
    }


    // https://blog.csdn.net/u010908723/article/details/79189221/
    // fileLock 同一java虚拟机下多进程有效  同进程多线程下无效，需要加锁，调用方法已加锁  (保证同机多应用可以使用不同workerId)
    //使用锁文件(.lock), 使得同进程内(fileLock同进程多线程下无效)，可以使用多个workId (保证统一应用可以使用不同workerId)
    protected Long readWorkerId(){
        //查找
        File workerIdFolder=new File(WORKER_ID_FOLDER);
        File[] files=workerIdFolder.listFiles((s)->s.getName().endsWith(WORKER_ID_FILE_NAME_SUFFIX));

        if(files==null || files.length==0){
            return null;
        }
        String content=null;
        RandomAccessFile raf=null;
        FileLock fl=null;
        try {
            for(File file:files){
                if(existLockFile(file)){
                    continue;
                }

                raf=new RandomAccessFile(file, rafMode);
                //获取workerId文件锁
                fl=raf.getChannel().tryLock();

                if(fl!=null){
                    //再次检查 锁文件(.lock)
                    if(existLockFile(file)){
                        fl.release();
                        continue;
                    }

                    workerIdFile=file;
                    content=raf.readLine();
                    //创建锁文件代替文件锁
                    workerIdLockFile=createLockFile(file);
                    fl.release();
                    break;
                }
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                if(fl!=null){
                    fl.release();
                }
                if(raf!=null) {
                    raf.close();
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }


        if(StringUtils.isEmpty(content)){
            return null;
        }
        Long workerId=Long.valueOf(content.trim());
        LOGGER.info("从文件{}读取到的workerId为{}",workerIdFile.getAbsolutePath(), workerId);
        return workerId;
    }


    protected void writeWorkerId(Long workerId){
        Assert.notNull(workerId, "[workerId]不能为空");

        File workerIdFolder=new File(WORKER_ID_FOLDER);
        File[] files=workerIdFolder.listFiles((s)->s.getName().endsWith(WORKER_ID_FILE_NAME_SUFFIX));
        int maxIndex=findMaxIndex(files);


        File file=null;
        RandomAccessFile raf=null;
        FileLock fl=null;

        try {
            while(true){
                maxIndex++;
                String workerFilePath=WORKER_ID_FOLDER+File.separator+
                        WORKER_ID_FILE_NAME_PREFIX + WORKER_ID_SEP + maxIndex + WORKER_ID_FILE_NAME_SUFFIX;

                file=new File(workerFilePath);
                if(existLockFile(file)){
                    continue;
                }

                raf=new RandomAccessFile(file, rafMode);
                fl=raf.getChannel().tryLock();

                if(fl!=null){
                    //再次检查 锁文件(.lock)
                    if(existLockFile(file)){
                        fl.release();
                        continue;
                    }

                    workerIdFile=file;
                    LOGGER.info("将workerId '{}' 写入文件{}",workerId,workerIdFile.getAbsolutePath());
                    raf.writeBytes(workerId.toString());

                    //创建锁文件代替文件锁
                    workerIdLockFile=createLockFile(file);
                    fl.release();
                    break;
                }

            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(fl!=null){
                    fl.release();
                }
                if(raf!=null) {
                    raf.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }

    @Override
    public void destroy() throws Exception {
        LOGGER.info("=================destroy=============");

        if(workerIdLockFile!=null){
            LOGGER.info("删除(.lock)文件{}",workerIdLockFile.getAbsolutePath());
            workerIdLockFile.delete();
        }
    }



    private int findMaxIndex(File[] files){
        if(files==null || files.length==0){
            return WORKER_ID_DEFAULT_INDEX;
        }
        int maxIndex=WORKER_ID_DEFAULT_INDEX;
        int index=0;
        for(File file: files){
            index=findIndexFromWorkerIdFile(file);
            if(maxIndex<index){
                maxIndex=index;
            }
        }
        return maxIndex;
    }


    private int findIndexFromWorkerIdFile(File workerFile){
        String name=workerFile.getName();
        String[] strs=name.split("\\.");
        String fileName=strs[0];
        String indexStr=fileName.replace(WORKER_ID_FILE_NAME_PREFIX+WORKER_ID_SEP,"");
        return Integer.parseInt(indexStr);
    }



    private boolean existLockFile(File workerFile){
        String workerFilePath=workerFile.getAbsolutePath();
        String workerFileLockPath=workerFilePath.replace(WORKER_ID_FILE_NAME_SUFFIX,WORKER_ID_FILE_LOCK_SUFFIX);
        File lockFile=new File(workerFileLockPath);

        return  lockFile.exists();
    }

    private File createLockFile(File workerFile){
        String workerFilePath=workerFile.getAbsolutePath();
        String workerFileLockPath=workerFilePath.replace(WORKER_ID_FILE_NAME_SUFFIX,WORKER_ID_FILE_LOCK_SUFFIX);
        File lockFile=new File(workerFileLockPath);

        try {
            FileOutputStream fos=new FileOutputStream(lockFile);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            throw new RuntimeException();
        }
        return lockFile;
    }

}
